package com.sd365.gateway.authorization.service.impl;


import cn.hutool.crypto.digest.opt.TOPT;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sd365.common.util.StringUtil;
import com.sd365.common.util.TokenUtil;
import com.sd365.gateway.authorization.dao.mapper.ResourceMapper;
import com.sd365.gateway.authorization.entity.Resource;
import com.sd365.gateway.authorization.service.AuthorizationService;
import com.sd365.gateway.authorization.service.ResourcesService;
import com.sd365.gateway.authorization.service.RoleResourcesService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.swing.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.util.regex.Pattern.compile;

/**
 * @author hantianbing
 * @version 1.0.0
 * @class WayBillServiceImpl
 * @classdesc 鉴权service实现类
 * @date 2022-12-29
 */
@Slf4j
@Service
public class AuthorizationServiceImpl implements AuthorizationService {
    /**
     * 匹配请求URL的正则表达式
     */
    private static final String AUTHOR_REQUEST_URL_EXPR="^https?:\\/\\/(?:[0-9a-zA-Z:\\.]*)([^\\?]*)";
    /**
     * 资源service
     */
    @Autowired
    private ResourcesService resourcesService;
    /**
     * 资源Mapper
     */
    @Autowired
    private ResourceMapper resourceMapper;
    /**
     * 角色资源Service
     */
    @Autowired
    private RoleResourcesService roleResourcesService;
    /**
     * 用于处理redis
     */
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 用于连接redis， 处理redis
     */
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 鉴权开关
     */
    @Value("${hasOpenAuthorization}")
    private Boolean hasOpenAuthorization;
    /**
     * 续期时间三天 单位毫秒 1000 * 60 * 60 * 24 * 3 = 86400
     */
    private static final Long PERIOD = 259200000L;
    /**
     * 一小时
     */
    private static final Long ONE_DAY = 86400000L;
    /**
     * 统一redis存取前缀
     */
    private static final String USER_TOKEN_KEY = "user:token";


    @Override
    public Boolean commonResource(String url) throws InterruptedException {
        //配置url
        url = matcherUrl(AUTHOR_REQUEST_URL_EXPR, url);
        //获取commonResources
        List<Resource> commonResources = getCommonResource();
        //判断有没有资源
        return hasResource(url, commonResources);
    }

    /***
     * @Description 根据规则返回处理之后的url
     * @param rule 规则
     * @param url  待处理的url
     * @return java.lang.String
     * @Date 2022/12/29 15:48
     * @Author hantianbing
     **/
    private String matcherUrl(String rule, String url){
        final Pattern compile = compile(rule);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            url = matcher.group(1);
        }
        return url;
    }

    /***
     * @Description 获取通用资源，避免网关鉴权访问数据库而影响请求过程
     * 该资源是用户中心初始化存储的，用于鉴别么有纳入权限体系的资源
     * @return java.util.List<com.sd365.gateway.authorization.entity.Resource>
     * @Date 2022/12/29 15:49
     * @Author hantianbing
     **/
    private List<Resource> getCommonResource() {
        List<Resource> commonResources =new ArrayList<>();
        try {
            //status=2当做缓存的key
            Object o = redisTemplate.opsForValue().get(String.valueOf(2));
            //获取全部的通用资源
            commonResources = JSONObject.parseArray(String.valueOf(o),Resource.class);

        } catch (Exception e) {
            log.error("通用资源缓存获取失败");
            throw new  RuntimeException("通用资源缓存获取失败",e);
        }
        if(CollectionUtils.isEmpty(commonResources)){
            commonResources = resourceMapper.commonResource();
        }
        return commonResources;
    }

    @Override
    public Boolean roleAuthorization(String token, String url) {
        //判断出入参数不能为空
        Assert.hasText(token,"token为空 roleAuthorization 参数异常");
        Assert.hasText(url,"url为空 roleAuthorization 参数异常");
        List<Long> roleIds = new LinkedList<>();
        //解析token
        String parseToken = parseToken(token);
        Long userId = getUserId(parseToken);
        //更新redis中的时间， 如果更新失败， 就返回false
        if (!refreshTokenRedis(userId.toString())){
            return false;
        }
        //nacos配置文件控制鉴权开关， 关闭则全部放行
        if (!hasOpenAuthorization){
            return true;
        }
        // 获取roleIds
        List<String> roleIdsList = getRoleIdsList(parseToken,roleIds);
        //提取url
        String authorURL = getMatchURL(url,AUTHOR_REQUEST_URL_EXPR);
        //获取角色资源
        List<Resource> resources = getResourceList(roleIdsList,roleIds);
        if (resources == null || resources.size()==0) {return false;}
        //判断是否有相应的资源权限
        return hasResource(authorURL, resources);
    }

    /***
     * @Description 判断有没有资源，
     * @param authorUrl 要认证的url
     * @param resources 拥有的资源
     * @return java.lang.Boolean
     * @Date 2022/12/29 15:50
     * @Author hantianbing
     **/
    private Boolean hasResource(String authorUrl, List<Resource> resources){
        //Url字符串做处理, 有一些请求会携带id， 把/id去掉
        while(authorUrl.charAt(authorUrl.length()-1) >='0' && authorUrl.charAt(authorUrl.length()-1) <='9'){
            authorUrl = authorUrl.substring(0, authorUrl.length()-1);
        }
        if(authorUrl.charAt(authorUrl.length()-1) == '/'){
            authorUrl = authorUrl.substring(0, authorUrl.length()-1);
        }
        //查看又没权限
        for (Resource resource : resources) {
            if (!StringUtils.isEmpty(resource.getApi())) {
                final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                final Matcher matcherApi = pattern.matcher(authorUrl);
                if (matcherApi.find()) {
                    return true;
                }
            }
        }
        return false;
    }

    /***
     * @Description 从缓存和数据库中获取resource
     * @param roleIdsList 用做key来从缓存中获取resource
     * @param roleIds     用于查询数据库中的resource
     * @return java.util.List<com.sd365.gateway.authorization.entity.Resource>
     * @Date 2022/12/29 15:54
     * @Author hantianbing
     **/
    private List<Resource> getResourceList(List<String> roleIdsList,List<Long> roleIds) {
        Assert.notEmpty(roleIdsList,"roleIdsList不能为空");
        Assert.notEmpty(roleIds,"roleIds不能为空");
       //查缓存或者数据库表鉴权
        List<Resource> resources = new ArrayList<>();
        try {
            //从redis中获取apis
            Set<String> apis = new HashSet<>();
            for(String roleId : roleIdsList){
                List<String> apiList = (List<String>) redisTemplate.opsForValue().get("roleApis:"+roleId);
                for (String api : apiList) {
                    apis.add(api);
                }
            }
            //封装成resource
            resources = apis.stream().map(api -> {
                Resource resource = new Resource();
                resource.setApi(api);
                return resource;
            }).collect(Collectors.toList());
        } catch (Exception e) {
            resources=null;
            log.info("缓存中没有对应的角色资源");
            e.printStackTrace();
        }
        if(CollectionUtils.isEmpty(resources)){
            //当redis中没有对应roleId的缓存或者redis异常时,鉴权服务访问数据库的资源表
            List<Long> resourceIds = roleResourcesService.searchResourceIdsByroleIds(roleIds);
            if (resourceIds == null || resourceIds.size() == 0) {
                return null;
            }
            resources = resourcesService.searchResource(resourceIds);
        }
        return resources;
    }

    /***
     * @Description     从token中解析出角色id的list
     * @param token     从header中解析的 accessToken
     * @param roleIds   roleIds
     * @return java.util.List<java.lang.String>
     * @Date 2022/12/29 15:55
     * @Author hantianbing
     **/
    private List<String> getRoleIdsList(String token,List<Long> roleIds) {
        String tokenCode = new String(Base64.getDecoder().decode(token.getBytes()));
        JSONObject tokenJson = JSONObject.parseObject(tokenCode);
        JSONArray roleIdsJson = (JSONArray) tokenJson.get((Object) "roleIds");
        List<String> roleIdsList = JSONArray.toJavaObject(roleIdsJson, List.class);
        if(!CollectionUtils.isEmpty(roleIdsList)){
            for (String roleIdStr : roleIdsList) {
                roleIds.add(Long.parseLong(roleIdStr));
            }
        }else {
            throw new RuntimeException("roleIdsList为空异常");
        }
        return roleIdsList;
    }
    
    /***
     * @Description 根据正则表达式匹配URL
     * @param url   URL
     * @param reg   正则表达式
     * @return java.lang.String
     * @Date 2022/12/29 15:57
     * @Author hantianbing
     **/
    private String getMatchURL(String url,String reg){
        Assert.hasText(url, "url NOT NULL");
        Assert.hasText(reg, "url NOT NULL");
        return matcherUrl(reg, url);
    }
    
    /***
     * @Description    更新用户在redis中的存储
     * @param userId   userId
     * @return java.lang.Boolean
     * @Date 2022/12/29 15:58
     * @Author hantianbing
     **/
    private Boolean refreshTokenRedis(String userId){
        Assert.hasText(userId,"userId为空, refreshTokenRedis()参数异常");
        // 从redis中获取token
        String tokenKey = USER_TOKEN_KEY + userId;
        String redisToken = stringRedisTemplate.opsForValue().get(tokenKey);
        // 如果redis中没有读取到token 说明token过期了，需要重新登陆
        if (StringUtil.isEmpty(redisToken)){
            return false;
        }
        String header = null;
        String playLoad = null;
        String[] headerAndPlayLoad = redisToken.split("\\.");
        if (headerAndPlayLoad.length > 1) {
            header = headerAndPlayLoad[0];
            playLoad = headerAndPlayLoad[1];
        }
        // 对playLoad进行解密，获取json对象
        JSONObject tokenJson = getTokenJson(playLoad);
        Assert.notNull(tokenJson,"tokenJson为空");
        Date expiresAt = getTokenExpiresAt(tokenJson);
        Assert.notNull(expiresAt,"expiresAt过期时间为空");
        if (expiresAt.before(new Date())){
            // 判断key是否存在
            if (stringRedisTemplate.hasKey(tokenKey)){
                // 更新token中过期时间
                JSONObject updateTokenExpiresAt = updateTokenExpiresAt(tokenJson);
                // 更新token
                String updateToken = updateToken(header, updateTokenExpiresAt);
                // 重置redis中过期时间
                stringRedisTemplate.opsForValue().set(tokenKey,updateToken,PERIOD + ONE_DAY,TimeUnit.MILLISECONDS);
                return true;
            }
            return false;
        }
        return true;
    }

    /***
     * @Description 获取token的到期时间
     * @param tokenJson token
     * @return java.util.Date
     * @Date 2022/12/29 16:02
     * @Author hantianbing
     **/
    private Date getTokenExpiresAt(JSONObject tokenJson){
        Assert.notNull(tokenJson,"tokenJson不能为空，getTokenExpiresAt()参数异常");
        Long expiresTime = (Long) tokenJson.get("expiresAt");
        Assert.notNull(expiresTime,"getTokenExpiresAt  expiresTime 获取时间失败");
        return new Date(expiresTime);
    }

    /***
     * @Description 更新token的过期时间
     * @param tokenJson token
     * @return com.alibaba.fastjson.JSONObject
     * @Date 2022/12/29 16:02
     * @Author hantianbing
     **/
    private JSONObject updateTokenExpiresAt(JSONObject tokenJson){
        Assert.notNull(tokenJson,"tokenJson为空 updateTokenExpiresAt参数错误");
        tokenJson.put("expiresAt",new Date(System.currentTimeMillis() + PERIOD));
        return tokenJson;
    }

    /***
     * @Description  更新token
     * @param header header头
     * @param playLoad  playLoad字段
     * @return java.lang.String
     * @Date 2022/12/29 16:02
     * @Author hantianbing
     **/
    private String updateToken(String header,JSONObject playLoad){
        Assert.hasText(header,"header 为空，updateToken参数异常");
        Assert.notNull(playLoad,"playLoad 为空，updateToken参数异常");
        String encoderToken = TokenUtil.encoderToken(playLoad.toJSONString());
        return header + "." + encoderToken;
    }


    /***
     * @Description 截取token的playload字段
     * @param token 传入的token
     * @return java.lang.String
     * @Date 2022/12/29 16:03
     * @Author hantianbing
     **/
    private String parseToken(String token){
        Assert.hasText(token,"token为空 parseToken 参数异常");
        String parseToken = null;
        String[] jwtTokeItem = token.split("\\.");
        if (jwtTokeItem.length > 1) {
            parseToken = jwtTokeItem[1];
        }else {
            throw new RuntimeException("roleAuthorization 解析的token的第2个元素不存在");
        }
        parseToken = parseToken.substring(0, parseToken.length() - 1);
        return parseToken;
    }

    /***
     * @Description 解析token获取JSON对象
     * @param token 传入的token
     * @return com.alibaba.fastjson.JSONObject
     * @Date 2022/12/29 16:04
     * @Author hantianbing
     **/
    private JSONObject getTokenJson(String token){
        Assert.hasText(token,"token为空 getTokenJson参数异常");
        String tokenCode = new String(Base64.getDecoder().decode(token.getBytes()));

        Assert.hasText(tokenCode,"getTokenJson tokenCode不能为空");
        return JSON.parseObject(tokenCode);
    }

    /***
     * @Description 从token中获取userId
     * @param token 传入的token
     * @return java.lang.Long
     * @Date 2022/12/29 16:06
     * @Author hantianbing
     **/
    private Long getUserId(String token){
        Assert.hasText(token,"token为空 getUserId() 参数异常");

        JSONObject tokenJson = getTokenJson(token);
        Assert.notNull(tokenJson,"tokenJson 不能为空");

        Long userId = (Long) tokenJson.get("userId");
        Assert.notNull(userId,"userId 不能为空");
        return userId;
    }
}
